from sqlalchemy import create_engine, Table, Column, String
from sqlalchemy.exc import IntegrityError
from functools import wraps

from scopt.util import message as msg_util

from scopt.util import sqlalchemy_util


def transaction(tx_members, required=True):
    ''' Ads transaction support to MPU handler

    @transaction(['lds/BDS'], required=False)
    def handle(data_packet)

    Decorator takes as argument list of transactional resources participating
    in transaction on MPU handler, and marker signaling if transaction is
    required. Until nested transactions will be supported, marker has no
    practical use.

    Prior to entering MPU handler, decorator initializes connection and
    transaction for each participating resource.
    '''
    def decorator(func):
        @wraps(func)
        def transaction_required(*args, **kwargs):
            data_packet = args[0]
            db = msg_util.get_db(data_packet)

            tx_owner = not db.tx_active()
            if tx_owner:
                db.tx_signal()

            for lds in tx_members:
                db.connect(lds)
                db.tx_begin(lds)
            try:
                result = func(*args, **kwargs)
                if tx_owner:
                    if msg_util.get_message_status(data_packet) == 'OK':
                        db.tx_commit_all()
                    else:
                        db.tx_rollback_all()
            except:
                if tx_owner:
                    db.tx_rollback_all()
                raise
            finally:
                if tx_owner:
                    db.finish()

            return result
        return transaction_required
    return decorator


# TODO:
#
# DO NOT REFAKTOR UNTIL SOME GENERIC ACCEPTABLE SOLUTION IS FOUND!!!
#
# Module is functional AS IS. After few functional use cases, overall review of
# module and its usage will need to be done.
#
# Nice to have: 2PC support
class DB:
    '''DB represents interface to database containing application business
    data. Is used directly by messsage handlers. DB class is initialized at
    application startup.

    Responsibilities of class DB.
    1. take care of database resources
    2. provide utility functions for interaction with database
    '''
    def __init__(self, lds_model):
        ''' DB is initialized with map of logical datasources and related
        models:

        lds_model = {'lds/BDS':<model>}
        self.__lds_model represents database model of logical datasource
        self.__resources represents actual connection and connection metadata
                         of logical datasource
        self.__in_transaction marker indicating if transaction is present
        '''
        self.__lds_model = self.__add_metainfo(lds_model)
        self.__resources = None
        self.__in_transaction = False
        self.__is_demo = False

    def __add_metainfo(self, lds_model):
        for metadata in lds_model.values():
            Table('metainfo', metadata,
                Column('name', String, nullable=False, unique=True),
                Column('value', String, nullable=False))
        return lds_model

    def init(self, data_packet):
        ''' Initialize DB
        Function is called at the time of data_packet arrival on business
        module handler.
        '''
        if 'context' in data_packet:
            self.__resources = data_packet['context']['resources']
            self.__is_demo = data_packet['demo']
            return msg_util.set_db(data_packet, self)
        else:
            return data_packet

    def connect(self, lds):
        resource = self.__resources[lds]
        if 'connection' in resource:
            return
        engine = create_engine(resource['url'], echo=True)
        resource['connection'] = engine.connect()

    def init_lds(self, lds='lds/BDS'):
        resource = self.__resources[lds]
        engine = create_engine(resource['url'], echo=True)
        self.__lds_model[lds].create_all(engine)

    def init_demo_data(self, demo_data, lds='lds/BDS'):
        connection = self.__resources[lds]['connection']
        metadata = self.__lds_model[lds]
        for data in demo_data:
            data(connection, metadata)

    def tx_signal(self):
        self.__in_transaction = True

    def tx_active(self):
        return self.__in_transaction

    def tx_begin(self, lds):
        resource = self.__resources[lds]
        if 'tx' not in resource:
            resource['tx'] = resource['connection'].begin()

    def tx_commit(self, lds):
        resource = self.__resources[lds]
        if 'tx' in resource:
            resource['tx'].commit()
            del resource['tx']

    def tx_commit_all(self):
        for resource in self.__resources.values():
            resource['tx'].commit()
            del resource['tx']

    def tx_rollback(self, lds):
        resource = self.__resources[lds]
        if 'tx' in resource:
            resource['tx'].rollback()
            del resource['tx']

    def tx_rollback_all(self):
        for resource in self.__resources.values():
            resource['tx'].rollback()
            del resource['tx']

    def finish(self):
        for resource in self.__resources.values():
            resource['connection'].close()
            del resource['connection']
        self.__resources = None
        self.__in_transaction = False

    def table(self, table_name, lds='lds/BDS'):
        return Table(table_name, self.__lds_model[lds])

    def execute(self, command, result_mapper=None, lds='lds/BDS'):
        resource = self.__resources[lds]
        result = resource['connection'].execute(command)
        if result_mapper is not None:
            rows = result_mapper(result)
            result.close()
        return result if result_mapper is None else rows

    def query(self, tables, columns=None, where=None, order=None,
              mapping=None, result_mapper=None, lds='lds/BDS'):
        q = sqlalchemy_util.query(self.__lds_model[lds], tables, columns,
                                  where, order, mapping)
        result = self.execute(q, result_mapper, lds)
        if result_mapper is None:
            rows = result.fetchall()
            result.close()
        else:
            rows = result
        return rows

    def query_by_pk(self, table_name, pk_value, columns=None, mapping=None,
                    result_mapper=None, lds='lds/BDS'):
        q = sqlalchemy_util.query_by_pk(self.__lds_model[lds], table_name,
                                        pk_value, columns, mapping)
        result = self.execute(q, result_mapper, lds)
        if result_mapper is None:
            rows = result.fetchall()
            result.close()
        else:
            rows = result
        return rows

    def insert(self, table_name, values, mapping=None, lds='lds/BDS'):
        insert = sqlalchemy_util.insert(self.__lds_model[lds],
                                        table_name,
                                        values,
                                        mapping)
        try:
            return self.execute(insert).inserted_primary_key[0]
        except IntegrityError:
            return ('ERROR', 'business.violation.unique_key', None)

    def update(self, table_name, values, where, mapping=None, lds='lds/BDS'):
        update_command =\
            sqlalchemy_util.update(self.__lds_model[lds], table_name, values,
                                   where, mapping)
        try:
            self.execute(update_command)
            return None
        except IntegrityError:
            return ('ERROR', 'business.violation.unique_key', None)

    def update_by_pk(self, table_name, pk_value, values,
                     mapping=None, lds='lds/BDS'):
        if pk_value is None:
            return ('ERROR', 'business.record_no.is_none', None)

        result = self.query_by_pk(table_name, pk_value, mapping=mapping,
                                  lds=lds)
        if len(result) == 0:
            return ('ERROR',
                    'business.record_no.is_invalid',
                    '{0}: {1}'.format(table_name, pk_value))

        u = sqlalchemy_util.update_by_pk(self.__lds_model[lds], table_name,
                                         pk_value, values, mapping)
        try:
            self.execute(u)
        except IntegrityError:
            return ('ERROR', 'business.violation.unique_key', None)

    def delete(self, table_name, where, mapping=None, lds='lds/BDS'):
        d = sqlalchemy_util.delete(self.__lds_model[lds], table_name,
                                   where, mapping)
        self.execute(d)

    def delete_by_pk(self, table_name, pk_value, mapping=None, lds='lds/BDS'):
        if pk_value is None:
            return ('ERROR', 'business.record_no.is_none', None)

        result = self.query_by_pk(table_name, pk_value, mapping=mapping,
                                  lds=lds)
        if len(result) == 0:
            return ('ERROR',
                    'business.record_no.is_invalid',
                    '{0}: {1}'.format(table_name, pk_value))

        d = sqlalchemy_util.delete_by_pk(self.__lds_model[lds], table_name,
                                         pk_value)
        self.execute(d)

    def and_(self, key_map, *args, **kwargs):
        cond = kwargs.get('cond')
        if cond is None:
            cond = []
        for key in args:
            if key in key_map:
                cond.append([key, key_map[key]])
        return cond

    def data_(self, key_map, *args):
        data = {}
        for key in args:
            if key in key_map:
                data.update({key: key_map[key]})
        return data

    # TODO: Move to util
    def reverse(self, seq):
        return {k: v for (k, v) in seq}
